/*
 * Copyright © 2016 SKSPRUCE TECHNOLOGIES and others.  All rights reserved.
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License v1.0 which accompanies this distribution,
 * and is available at http://www.eclipse.org/legal/epl-v10.html
 */
package com.skspruce.sdn.impl;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Future;

import org.apache.commons.lang3.StringUtils;
import org.opendaylight.controller.md.sal.binding.api.DataBroker;
import org.opendaylight.controller.md.sal.binding.api.DataChangeListener;
import org.opendaylight.controller.md.sal.binding.api.WriteTransaction;
import org.opendaylight.controller.md.sal.common.api.data.AsyncDataBroker.DataChangeScope;
import org.opendaylight.controller.md.sal.common.api.data.AsyncDataChangeEvent;
import org.opendaylight.controller.md.sal.common.api.data.LogicalDatastoreType;
import org.opendaylight.controller.sal.binding.api.BindingAwareBroker.ProviderContext;
import org.opendaylight.controller.sal.binding.api.BindingAwareBroker.RpcRegistration;
import org.opendaylight.controller.sal.binding.api.BindingAwareProvider;
import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.inet.types.rev100924.Ipv4Prefix;
import org.opendaylight.yang.gen.v1.urn.skspruce.yang.sagitta2.rev160320.DeleteDhcpStatisticsInput;
import org.opendaylight.yang.gen.v1.urn.skspruce.yang.sagitta2.rev160320.DhcpStatistics;
import org.opendaylight.yang.gen.v1.urn.skspruce.yang.sagitta2.rev160320.Pools;
import org.opendaylight.yang.gen.v1.urn.skspruce.yang.sagitta2.rev160320.Sagitta2Service;
import org.opendaylight.yang.gen.v1.urn.skspruce.yang.sagitta2.rev160320.SetDhcpStatisticsInput;
import org.opendaylight.yang.gen.v1.urn.skspruce.yang.sagitta2.rev160320.dhcp.statistics.Statistic;
import org.opendaylight.yang.gen.v1.urn.skspruce.yang.sagitta2.rev160320.dhcp.statistics.StatisticBuilder;
import org.opendaylight.yang.gen.v1.urn.skspruce.yang.sagitta2.rev160320.dhcp.statistics.StatisticKey;
import org.opendaylight.yang.gen.v1.urn.skspruce.yang.sagitta2.rev160320.pools.Pool;
import org.opendaylight.yang.gen.v1.urn.skspruce.yang.sagitta2.rev160320.pools.PoolKey;
import org.opendaylight.yang.gen.v1.urn.skspruce.yang.sagitta2.rev160320.pools.pool.Lease;
import org.opendaylight.yangtools.concepts.ListenerRegistration;
import org.opendaylight.yangtools.yang.binding.DataObject;
import org.opendaylight.yangtools.yang.binding.InstanceIdentifier;
import org.opendaylight.yangtools.yang.common.RpcError;
import org.opendaylight.yangtools.yang.common.RpcError.ErrorType;
import org.opendaylight.yangtools.yang.common.RpcResult;
import org.opendaylight.yangtools.yang.common.RpcResultBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.SettableFuture;

public class Sagitta2Provider implements BindingAwareProvider, AutoCloseable, DataChangeListener, Sagitta2Service {

	private static final Logger LOG = LoggerFactory.getLogger(Sagitta2Provider.class);

	private static String cmdPrefixion = "/opt/ovsdb-client transact tcp:127.0.0.1:6640";

	public static final InstanceIdentifier<Pools> POOLS_IID = InstanceIdentifier.builder(Pools.class).build();

	private ListenerRegistration<DataChangeListener> poolsDCReg;
	
	private RpcRegistration<Sagitta2Service> rpcReg;

	private DataBroker dataBroker;

	@Override
	public void onSessionInitiated(ProviderContext session) {
		this.dataBroker = session.getSALService(DataBroker.class);

		// Register the DataChangeListener for Pools configuration subtree
		poolsDCReg = dataBroker.registerDataChangeListener(LogicalDatastoreType.CONFIGURATION, POOLS_IID, this,
				DataChangeScope.SUBTREE);

		// Register the RPC Service
		rpcReg = session.addRpcImplementation(Sagitta2Service.class, this);

		LOG.info("Sagitta2Provider Session Initiated");
	}

	@Override
	public void close() throws Exception {
        if (poolsDCReg != null)
		    poolsDCReg.close();
        if (rpcReg != null)
		    rpcReg.close();
		LOG.info("Sagitta2Provider Closed");
	}

	@Override
	public void onDataChanged(AsyncDataChangeEvent<InstanceIdentifier<?>, DataObject> change) {
		LOG.debug("===Data changed: {} created, {} updated, {} removed", change.getCreatedData().size(),
				change.getUpdatedData().size(), change.getRemovedPaths().size());

		DataObject dataObject;

		// Iterate over any created nodes or interfaces
		for (Map.Entry<InstanceIdentifier<?>, DataObject> entry : change.getCreatedData().entrySet()) {
			dataObject = entry.getValue();
			LOG.debug("\n===ADDED Path {}, Object {}, class {}", entry.getKey(), dataObject, dataObject.getClass());
			// If Pool added, call insertPoolToOvsDb()
			if (dataObject instanceof Pool) {
				insertPoolToOvsDb((Pool) dataObject);
			}
			// If Lease added, skip
		}

		// Iterate over any deleted nodes or interfaces
		Map<InstanceIdentifier<?>, DataObject> originalData = change.getOriginalData();
		for (InstanceIdentifier<?> path : change.getRemovedPaths()) {
			dataObject = originalData.get(path);
			LOG.debug("\n===REMOVED Path {}, Object {}", path, dataObject);
			// If Pool removed, call removePoolToOvsDb()
			if (dataObject instanceof Pool) {
				Pool pool = (Pool) dataObject;
				removePoolToOvsDb(pool.getKey());
			}
		}

		for (Map.Entry<InstanceIdentifier<?>, DataObject> entry : change.getUpdatedData().entrySet()) {
			dataObject = entry.getValue();
			DataObject originalDataObject = originalData.get(entry.getKey());
			LOG.debug("\n===UPDATED Path {}, New Object {}, Old Object {}", entry.getKey(), dataObject,
					originalDataObject);
			// If Pool updated, call updatePoolToOvsDb()
			if (dataObject instanceof Pool) {
				updatePoolToOvsDb((Pool) dataObject);
			}
			// If Lease added, call updatePoolToOvsDb()
			if (dataObject instanceof Lease) {
				Lease lease = (Lease) dataObject;
				// TODO
			}
		}

	}

	private void updatePoolToOvsDb(Pool pool) {
		Ipv4Prefix net = pool.getNetwork();
		Lease lease = pool.getLease();
		StringBuffer buf = new StringBuffer();

		/* update dhcp pool network */
		buf.append("[\"dhcp\", {\"op\":\"update\",\"table\":\"Dhcppool\", " + "\"where\":[[\"name\", \"==\", \""
				+ pool.getName().getValue() + "\"]]" + ",\"row\":{");

		if (null != net && null != lease) {
			buf.append("\"network\":\"" + net.getValue() + "\"");
			buf.append(",\"lease\":[\"set\", [[\"named-uuid\", \"leaseinfo\"]]]}}");
			buf.append(",{\"op\":\"insert\", \"table\":\"lease_info\", " + "\"row\":{\"day\":" + lease.getDays()
					+ ", \"hour\":" + lease.getHours() + ", \"minute\":" + lease.getMinutes() + ", \"second\":"
					+ lease.getSeconds() + "}, \"uuid-name\":\"leaseinfo\"}]");
		} else if (null != net) {
			buf.append("\"network\":\"" + net.getValue() + "\"}}]");
		} else if (null != lease) {
			buf.append("\"lease\":[\"set\", [[\"named-uuid\", \"leaseinfo\"]]]}}");
			buf.append(",{\"op\":\"insert\", \"table\":\"lease_info\", " + "\"row\":{\"day\":" + lease.getDays()
					+ ", \"hour\":" + lease.getHours() + ", \"minute\":" + lease.getMinutes() + ", \"second\":"
					+ lease.getSeconds() + "}, \"uuid-name\":\"leaseinfo\"}]");
		} else {
			buf.append("}}]'");
		}

		runCmd(buf.toString());

	}

	private void removePoolToOvsDb(PoolKey key) {
		StringBuffer buf = new StringBuffer();

		/* delete dhcp pool */
		buf.append("[\"dhcp\", {\"op\":\"delete\",\"table\":\"Dhcppool\", \"where\":[[\"name\", \"==\", \""
				+ key.getName().getValue() + "\"]]}]");

		runCmd(buf.toString());

	}

	private void insertPoolToOvsDb(Pool pool) {
		Ipv4Prefix net = pool.getNetwork();
		Lease lease = pool.getLease();
		StringBuffer buf = new StringBuffer();

		/* insert dhcp pool */
		buf.append("[\"dhcp\", {\"op\":\"insert\",\"table\":\"Dhcppool\", \"row\":{\"name\":\""
				+ pool.getName().getValue() + "\"");

		if (null != net) {
			buf.append(",\"network\":\"" + net.getValue() + "\"");
		}

		if (null != lease) {
			buf.append(",\"lease\":[\"set\", [[\"named-uuid\", \"leaseinfo\"]]]}}");
			buf.append(",{\"op\":\"insert\", \"table\":\"lease_info\", " + "\"row\":{\"day\":" + lease.getDays()
					+ ", \"hour\":" + lease.getHours() + ", \"minute\":" + lease.getMinutes() + ", \"second\":"
					+ lease.getSeconds() + "}, \"uuid-name\":\"leaseinfo\"}]");
		} else {
			buf.append("}}]");
		}

		runCmd(buf.toString());

	}

	private static void runCmd(String cmd) {
		List<String> list = new ArrayList<String>();
		for (String s : cmdPrefixion.split(" ")) {
			if (StringUtils.isNotBlank(s)) {
				list.add(s);
			}
		}
		list.add(cmd);
		String[] cmds = list.toArray(new String[list.size()]);

		try {
			LOG.info("run:{}", list);
			Process ps = Runtime.getRuntime().exec(cmds);
			ps.waitFor();

			StringBuffer sb = new StringBuffer();
			readString(new BufferedReader(new InputStreamReader(ps.getInputStream())), sb);
			sb.append(System.getProperty("line.separator"));
			readString(new BufferedReader(new InputStreamReader(ps.getErrorStream())), sb);
			sb.append(System.getProperty("line.separator"));
			LOG.info("cmd result:{}", sb.toString());

		} catch (IOException | InterruptedException e) {
			e.printStackTrace();
			LOG.error("Failed to run cmd:{}", cmds, e);
		}
	}

	private static void readString(BufferedReader br, StringBuffer sb) throws IOException {
		String line;
		while ((line = br.readLine()) != null) {
			sb.append(line);
		}
	}

	@Override
	public Future<RpcResult<Void>> deleteDhcpStatistics(DeleteDhcpStatisticsInput input) {
		LOG.info("Calling deleteDhcpStatistics.  - input:{}", input);

		// Delete data from the MD-SAL data store
		StatisticKey key = new StatisticKey(input.getNetPrefix());
		InstanceIdentifier<Statistic> path = InstanceIdentifier.builder(DhcpStatistics.class)
				.child(Statistic.class, key).build();

		final SettableFuture<RpcResult<Void>> futureResult = SettableFuture.create();

		WriteTransaction tx = dataBroker.newWriteOnlyTransaction();
		tx.delete(LogicalDatastoreType.OPERATIONAL, path);

		Futures.addCallback(tx.submit(), new FutureCallback<Void>() {
			@Override
			public void onSuccess(final Void result) {
				LOG.info("transaction result: {}", result);
				futureResult.set(RpcResultBuilder.<Void> success().build());
			}

			@Override
			public void onFailure(final Throwable t) {
				LOG.error("transaction failed", t);
				futureResult
						.set(RpcResultBuilder.<Void> failed().withError(ErrorType.APPLICATION, t.getMessage()).build());
			}
		});

		return futureResult;
	}

	@Override
	public Future<RpcResult<Void>> setDhcpStatistics(SetDhcpStatisticsInput input) {
		LOG.info("Calling setDhcpStatistics.  - input:{}", input);

		StatisticKey key = new StatisticKey(input.getNetPrefix());
		final Statistic entry = new StatisticBuilder().setKey(key).setNetPrefix(input.getNetPrefix())
				.setTotalLease(input.getTotalLease()).setActiveLease(input.getActiveLease()).build();

		final SettableFuture<RpcResult<Void>> futureResult = SettableFuture.create();

		// Each entry will be identifiable by a unique key, we have to create
		// that identifier
		InstanceIdentifier.InstanceIdentifierBuilder<Statistic> entryIdBuilder = InstanceIdentifier
				.<DhcpStatistics> builder(DhcpStatistics.class).child(Statistic.class, entry.getKey());
		InstanceIdentifier<Statistic> path = entryIdBuilder.build();
		// Place entry in data store tree
		WriteTransaction tx = dataBroker.newReadWriteTransaction();
		tx.put(LogicalDatastoreType.OPERATIONAL, path, entry, true);
		Futures.addCallback(tx.submit(), new FutureCallback<Void>() {
			@Override
			public void onSuccess(final Void result) {
				LOG.info("transaction result: {}", result);
				futureResult.set(RpcResultBuilder.<Void> success().build());
			}

			@Override
			public void onFailure(final Throwable t) {
				LOG.info("transaction failed", t);
				futureResult.set(RpcResultBuilder.<Void> failed()
						.withError(RpcError.ErrorType.APPLICATION, t.getMessage()).build());
			}
		});

		return futureResult;
	}

}
